/*      $Id: mod_cgi.c 1098 2010-05-12 03:36:55Z phrakt $    */
/*
 * Copyright (c) 2007 Jean-Francois Brousseau <phrakt@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <http/http.h>
#include <http/msg.h>
#include <http/addr.h>
#include <http/sock.h>
#include <http/entity.h>

#include <canopy/log.h>

#include <httpd/httpd.h>


#define CGI_VERSION_STRING	"CGI/1.1"

#define CGI_MEDIA_TYPE		"application/x-httpd-cgi"


int   mod_cgi_init     (void);
void  mod_cgi_cleanup  (void);
int   mod_cgi_handler  (struct httpd_hdlr_args *);

static int  mod_cgi_setupenv (http_sock_t *, http_msg_t *);

static int   mod_cgi_setenv   (const char *, const char *, ...);

/*
 * mod_cgi_init()
 *
 * Setup the CGI environment.
 */
int
mod_cgi_init(void)
{
	if (canopy_addhdlr(HTTPD_PHASE_RESPONSE, HTTPD_HDLR_HEAD,
	    "mod_cgi_response", mod_cgi_handler, NULL) == -1) {
		return (-1);
	}

	return (0);
}

/*
 * mod_cgi_cleanup()
 *
 */
void
mod_cgi_cleanup(void)
{

}

/*
 * mod_cgi_handler()
 *
 * Main handler for CGI support
 */
int
mod_cgi_handler(struct httpd_hdlr_args *args)
{
	if (strcmp(args->ha_resp->msg_entity->ent_media, CGI_MEDIA_TYPE) != 0)
		return (HTTPD_HDLR_OK);

	mod_cgi_exec(args->ha_sock, args->ha_req, args->ha_resp);

	return (HTTPD_HDLR_OK | HTTPD_HDLR_STOPPHASE);
}

/*
 * mod_cgi_exec()
 *
 *
 */
int
mod_cgi_exec(http_sock_t *sock, http_msg_t *req, http_msg_t *resp)
{
	int fd, argc, status;
	pid_t pid;
	char *argv[16];

	cnp_log(httpd_logchan, CNP_LOG_DEBUG, "forking new process");
	if ((pid = fork()) == -1) {
		cnp_log(httpd_logchan, CNP_LOG_ERR, "failed to fork for CGI");
		return (-1);
	}

	if (pid == 0) {
		argc = 0;
		argv[argc++] = resp->msg_entity->ent_path;
		argv[argc++] = NULL;

		/* we're in the child, remap the descriptors */
		fd = http_sock_getfd(sock);
		dup2(fd, STDIN_FILENO);
		dup2(fd, STDOUT_FILENO);

		/* set CGI-related the environment variables */
		mod_cgi_setupenv(sock, req);

		execv(argv[0], argv);
	}
	else {
		/* we're in the parent, wait for the child to finish */
		waitpid(pid, &status, 0);
	}

	return (0);
}

/*
 * mod_cgi_setupenv()
 *
 * Setup the environment of a subprocess according to the CGI specification.
 */
static int
mod_cgi_setupenv(http_sock_t *sock, http_msg_t *req)
{
	int port;
	char addrbuf[64];
	uint64_t size = 0;
	http_uri_t *uri = req->msg_req.req_url;
	http_sockaddr_t addr;

	clearenv();
	mod_cgi_setenv("GATEWAY_INTERFACE", CGI_VERSION_STRING);
	mod_cgi_setenv("SERVER_SOFTWARE", "mod_cgi/0.1");

	mod_cgi_setenv("SERVER_NAME", http_uri_gethost(uri));

	/* request-related variables */
	if (req->msg_version == HTTP_VERSION_09)
		mod_cgi_setenv("SERVER_PROTOCOL", "HTTP/0.9");
	else if (req->msg_version == HTTP_VERSION_10)
		mod_cgi_setenv("SERVER_PROTOCOL", "HTTP/1.0");
	else if (req->msg_version == HTTP_VERSION_11)
		mod_cgi_setenv("SERVER_PROTOCOL", "HTTP/1.1");

	if (http_sock_getraddr(sock, &addr) == -1) {
		canopy_log(CNP_LOG_ERR, "failed to get remote socket address");
		return (-1);
	}

	if (http_addr_ntop(&addr, addrbuf, sizeof(addrbuf)) == NULL) {
		canopy_log(CNP_LOG_ERR, "failed to format remote address");
		return (-1);
	}

	if ((port = http_addr_getport(&addr)) == -1) {
		canopy_log(CNP_LOG_ERR, "failed to get remote socket port");
		return (-1);
	}

	if ((mod_cgi_setenv("SERVER_PORT", "%d", port) == -1) ||
	    (mod_cgi_setenv("REQUEST_METHOD", "GET") == -1) ||
	    (mod_cgi_setenv("REMOTE_ADDR", addrbuf) == -1) ||
	    (mod_cgi_setenv("REMOTE_HOST", "") == -1) ||
	    (mod_cgi_setenv("SCRIPT_NAME", http_uri_getpath(uri)) == -1) ||
	    (mod_cgi_setenv("PATH_INFO", "") == -1) ||
	    (mod_cgi_setenv("PATH_TRANSLATED", "") == -1))
		return (-1);

	if (http_uri_getquery(uri) == NULL)
		mod_cgi_setenv("QUERY_STRING", "");
	else
		mod_cgi_setenv("QUERY_STRING", http_uri_getquery(uri));

	/* auth-related variables */
	if (req->msg_req.req_auth != NULL) {
		mod_cgi_setenv("REMOTE_USER",
		    http_auth_getuser(req->msg_req.req_auth));
		mod_cgi_setenv("AUTH_TYPE",
		    http_auth_gettype(req->msg_req.req_auth));
	}

	/* content-related variables */
	if (req->msg_entity != NULL) {
		http_entity_getsize(req->msg_entity, &size);
		mod_cgi_setenv("CONTENT_LENGTH", "%llu", size);
		mod_cgi_setenv("CONTENT_TYPE", req->msg_entity->ent_media);
	}

	return (0);
}

/*
 * mod_cgi_setenv()
 *
 *
 */
static int
mod_cgi_setenv(const char *name, const char *fmt, ...)
{
	int ret;
	va_list vap;
	char value[64];

	va_start(vap, fmt);

	if (((ret = vsnprintf(value, sizeof(value), fmt, vap)) == -1) ||
	    (ret >= sizeof(value))) {
		canopy_log(CNP_LOG_ERR, "failed to format environment value");
		return (-1);
	}

	va_end(vap);

	if ((ret = setenv(name, value, 1)) == -1)
		canopy_log(CNP_LOG_ERR,
		    "failed to set environment variable `%s'", name);

	return (ret);
}
